/*
Copyright (c) 2005-2006, MentorGen, LLC
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

+ Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
+ Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.
+ Neither the name of MentorGen LLC nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.
 */
package com.mentorgen.tools.profile.instrument;


import java.util.List;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.LocalVariablesSorter;
import org.objectweb.asm.signature.SignatureReader;

import com.mentorgen.tools.profile.Controller;
import com.mentorgen.tools.profile.runtime.EntityDicHelper;

import static org.objectweb.asm.Opcodes.INVOKESTATIC;

/**
 * This class is responsible for instrumenting a method to
 * call the profiler in order for performance
 * data to be gathered. The basic idea is that the profiler is called
 * when a method starts and when it exists which allows the profiler
 * to gather performance data (note that a method can be exited from
 * when an exception is thrown as well as when return is called). The
 * one big caveate is static initializers. They are not called as part
 * of the flow of the program &mdash; they are called by the classloader.
 * Since they whole premise of the profiler is built on the idea of a
 * orderly call stack, static initializers are not instrumented.
 *
 * @author Andrew Wilcox
 *
 */
public class PerfMethodAdapter extends MethodAdapter {
	private String _className, _methodName, _loaderID, _bundleName, _superName;
	private String _interfaces;
	private boolean _clinit = false;
	private boolean _init = false;

	// LocalVariableSorter _lvs is initialized from within the PerfClassAdapter as the VariableSorter for
	// the class is accessible from there. So, even though this variable is defined here, its initialization
	// happens outside this class which is quite tricky and confusing.
	public LocalVariablesSorter _lvs;
	
	private int totalSize = 0;

	public PerfMethodAdapter(
			MethodVisitor visitor,
			String loaderID,
			String bundleName,
			String className,
			String superName,
			String interfaces,
			String methodName) {
		
		super(visitor);
		_className = className;
		_superName = superName;
		_interfaces = interfaces;
		_methodName = methodName;
		_loaderID = loaderID;
		_bundleName = bundleName;

		// Static initializers are excluded. The reason for this
		// is the the profiling algorithm we're using mirrors the call stack.
		// Since static initializers are called by the classloader
		// and therefore aren't part of the programs flow of control,
		// static initializers can really mess up the profiler, especially
		// when they're called before the program's flow of control is started
		// (for example, the when the class with the main() method has a
		// static initalizer). So yes, this is a short comming in the
		// design of the profiler, but we're willing to live with it because
		// this profiler is lightweight and allows us to use it interactively.
		//
		if (methodName.equals("<clinit>")) {
			_clinit = true;
		} else if (methodName.startsWith("<init>")) {
			_init = true;
		}
	}

	public void visitCode() {
		if (_clinit) {
			super.visitCode();
			return;
		}

		// Because the alloc method looks at the class + method of the caller
		// this call needs to come before the call to Profile.start
		//

		if (Controller._trackObjectAlloc && _init) {
			this.visitLdcInsn(_className);
			this.visitMethodInsn(INVOKESTATIC,
					Controller._profiler,
					"alloc",
					"(Ljava/lang/String;)V");
		}
		
		
		// adding a local variable to every method to measure the bandwidth of methods it calls
		totalSize = _lvs.newLocal(Type.LONG_TYPE);

		//receiving the information for loaderID, bundleName, className, methodName
		this.visitLdcInsn(_loaderID);
		this.visitLdcInsn(_bundleName);
		this.visitLdcInsn(_className);
		this.visitLdcInsn(_superName);
		this.visitLdcInsn(_interfaces);
		this.visitLdcInsn(_methodName);
		this.visitMethodInsn(INVOKESTATIC,
				Controller._profiler, "start",
				"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
		
		super.visitCode();
	}

	public void visitInsn(int inst) {
		//TODO nimak - check to see if it solved the problem in any way?! 
		if (_clinit) {
			super.visitInsn(inst);
			return;
		}

		switch (inst) {
		case Opcodes.ARETURN:
		case Opcodes.DRETURN:
		case Opcodes.FRETURN:
		case Opcodes.IRETURN:
		case Opcodes.LRETURN:
		case Opcodes.RETURN:
		case Opcodes.ATHROW:

			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);

			this.visitMethodInsn(INVOKESTATIC,
					Controller._profiler,
					"end",
					"(Ljava/lang/String;Ljava/lang/String;)V");
			
			break;

		default:
			break;
		}

		if (Opcodes.MONITORENTER == inst) {
			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);

			this.visitMethodInsn(INVOKESTATIC,
					Controller._profiler,
					"beginWait",
					"(Ljava/lang/String;Ljava/lang/String;)V");

			super.visitInsn(inst);

			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);

			this.visitMethodInsn(INVOKESTATIC,
					Controller._profiler,
					"endWait",
					"(Ljava/lang/String;Ljava/lang/String;)V");
		} else {
			super.visitInsn(inst);
		}
	}
	
	@Override
	public void visitMethodInsn(int opcode, String owner, String name, String desc) {
		
		if (_clinit) {
			super.visitMethodInsn(opcode, owner, name, desc);
			return;
		}

		visitMethodInBandwidth(opcode, owner, name, desc);
		
		if (isWaitInsn(opcode, owner, name, desc)) {

			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);

			this.visitMethodInsn(INVOKESTATIC,
					Controller._profiler,
					"beginWait",
					"(Ljava/lang/String;Ljava/lang/String;)V");

			super.visitMethodInsn(opcode, owner, name, desc);

			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);

			this.visitMethodInsn(INVOKESTATIC,
					Controller._profiler,
					"endWait",
					"(Ljava/lang/String;Ljava/lang/String;)V");
		} else {
			super.visitMethodInsn(opcode, owner, name, desc);
		}
		
		visitMethodOutBandwidth(opcode, owner, name, desc);
	}
	
	private void visitMethodInBandwidth(int opcode, String owner, String name, String desc){

		// Decides based on the profiling configurations in the profile.properties file on whether
		// or not bandwidth profiling should happen for methods being called from within the method
		if (opcode == Opcodes.INVOKESPECIAL || !Controller._bwfilter.allow(owner))
			return;
		
		Type[] types = Type.getArgumentTypes(desc);
		PrefSignatureVisitor sigVisitor = parseAndMeasureBandwidth(opcode, name, desc);
			
		// The following four lines the first condition added to the body of instrumented code to ensure that
		// the switch for storing bandwidth is turned on
//			super.visitFrame(Opcodes.F_APPEND,1, new Object[] {Opcodes.INTEGER}, 0, null);
		super.visitFieldInsn(Opcodes.GETSTATIC, "com/mentorgen/tools/profile/Controller", "_bandwidth", "Z");
		Label controllerBWLabel = new Label();
		super.visitJumpInsn(Opcodes.IFEQ, controllerBWLabel);
		
		// The following fifteen lines add filtering for bandwidth data exchange
		// based on the desired level of granularity as specified in the config file for the profiler. 
		// It could be at the level of methods, classes, or class loaders
		super.visitFieldInsn(Opcodes.GETSTATIC, 
				"com/mentorgen/tools/profile/Controller", "_bwfilter", 
				"Lcom/mentorgen/tools/profile/instrument/bwfilter/BandwidthFilter;");
		super.visitInsn(Opcodes.ICONST_2);
		super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
		super.visitInsn(Opcodes.DUP);
		super.visitInsn(Opcodes.ICONST_0);
		super.visitLdcInsn(_className);
		super.visitInsn(Opcodes.AASTORE);
		super.visitInsn(Opcodes.DUP);
		super.visitInsn(Opcodes.ICONST_1);
		super.visitLdcInsn(owner);
		super.visitInsn(Opcodes.AASTORE);
		super.visitMethodInsn(Opcodes.INVOKEINTERFACE, 
				"com/mentorgen/tools/profile/instrument/bwfilter/BandwidthFilter", 
				"accept", "([Ljava/lang/Object;)Z");
		super.visitJumpInsn(Opcodes.IFEQ, controllerBWLabel);
		Label insideCode = new Label();
		super.visitLabel(insideCode);
		
		if (Controller._debug)
			System.out.println(_className + " => " + owner + "[" + name + " : "  + desc + "]");
		
		
		// Once all the conditions are met, the following lines start inserting code inside the methods
		// for the class to measure bandwidth exchange as desired by the accept method
		// from the desired BandwidthFilter class
		List<String> sigList = sigVisitor.getSigList();
		int[] localVarIndxArray = new int[sigList.size()];
		
		super.visitInsn(Opcodes.LCONST_0);
		super.visitVarInsn(Opcodes.LSTORE, totalSize);

		for (int i = sigList.size() - 2; i >= 0; i--){
			
			String type = types[i].toString();

			if (type.length() == 1){
				switch(type.charAt(0)){
				case 'Z':
					localVarIndxArray[i] = _lvs.newLocal(Type.BOOLEAN_TYPE);
					this.visitVarInsn(Opcodes.ISTORE, localVarIndxArray[i]);
					break;
				case 'C':
					localVarIndxArray[i] = _lvs.newLocal(Type.CHAR_TYPE);
					this.visitVarInsn(Opcodes.ISTORE, localVarIndxArray[i]);
					break;
				case 'B':
					localVarIndxArray[i] = _lvs.newLocal(Type.BYTE_TYPE);
					this.visitVarInsn(Opcodes.ISTORE, localVarIndxArray[i]);
					break;
				case 'S':
					localVarIndxArray[i] = _lvs.newLocal(Type.SHORT_TYPE);
					this.visitVarInsn(Opcodes.ISTORE, localVarIndxArray[i]);
					break;
				case 'I':
					localVarIndxArray[i] = _lvs.newLocal(Type.INT_TYPE);
					this.visitVarInsn(Opcodes.ISTORE, localVarIndxArray[i]);
					break;
				case 'F':
					localVarIndxArray[i] = _lvs.newLocal(Type.FLOAT_TYPE);
					this.visitVarInsn(Opcodes.FSTORE, localVarIndxArray[i]);
					break;
				case 'J':
					localVarIndxArray[i] = _lvs.newLocal(Type.LONG_TYPE);
					this.visitVarInsn(Opcodes.LSTORE, localVarIndxArray[i]);
					break;
				case 'D':
					localVarIndxArray[i] = _lvs.newLocal(Type.DOUBLE_TYPE);
					this.visitVarInsn(Opcodes.DSTORE, localVarIndxArray[i]);
					break;
				}
			}else{
				localVarIndxArray[i] = _lvs.newLocal(types[i]);
				this.visitVarInsn(Opcodes.ASTORE, localVarIndxArray[i]);
				this.visitVarInsn(Opcodes.ALOAD, localVarIndxArray[i]);
				this.visitMethodInsn(Opcodes.INVOKESTATIC,
						"com/mentorgen/tools/profile/instrument/memory/MemoryCounter", "estimate", "(Ljava/lang/Object;)J");
				this.visitVarInsn(Opcodes.LLOAD, totalSize);
				this.visitInsn(Opcodes.LADD);
				this.visitVarInsn(Opcodes.LSTORE, totalSize);
			}
		}

		// The following lines add the size of primitive data types obtained from analyzing the signature for the
		// methods to the totalSize variable as evaluated by analyzing the complex data types during the previous
		// process.
		this.visitIntInsn(Opcodes.SIPUSH, sigVisitor.getSigSize());
		this.visitInsn(Opcodes.I2L);
		this.visitVarInsn(Opcodes.LLOAD, totalSize);
		this.visitInsn(Opcodes.LADD);
		this.visitVarInsn(Opcodes.LSTORE, totalSize);
		
		// A print out of the collected information added to the bytecode of the original method files.
//			super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");
//			super.visitLdcInsn(owner + "[" + name + " " + desc + "]" + ": ");
//			super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V");
//			super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");
//			super.visitVarInsn(Opcodes.LLOAD, totalSize);
//			super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V");
		
		// The following lines reconstruct the stack by putting the variables back on the stack
		// for the analyzed method in the reverse order of how they were read from the stack.
		
		this.visitLdcInsn(_className);
		this.visitLdcInsn(_methodName);
		this.visitLdcInsn(owner);
		this.visitLdcInsn(name);
		this.visitVarInsn(Opcodes.LLOAD, totalSize);
		this.visitInsn(Opcodes.ICONST_1);
		super.visitMethodInsn(INVOKESTATIC, 
				"com/mentorgen/tools/profile/runtime/EntityDicHelper", "addMethod2MethodData", 
				"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V");
		
		for (int i = 0; i < sigList.size() - 1; i++){
			String type = types[i].toString();
			if (type.length() == 1){
				switch(type.charAt(0)){
				case 'Z':
				case 'C':
				case 'B':
				case 'S':
				case 'I':
					this.visitVarInsn(Opcodes.ILOAD, localVarIndxArray[i]);
					break;
				case 'F':
					this.visitVarInsn(Opcodes.FLOAD, localVarIndxArray[i]);
					break;
				case 'J':
					this.visitVarInsn(Opcodes.LLOAD, localVarIndxArray[i]);
					break;
				case 'D':
					this.visitVarInsn(Opcodes.DLOAD, localVarIndxArray[i]);
					break;
				}
			} else
				this.visitVarInsn(Opcodes.ALOAD, localVarIndxArray[i]);
		}
		
		super.visitLabel(controllerBWLabel);
		
	}
	
	private void visitMethodOutBandwidth (int opcode, String owner, String name, String desc){
		
		// gets the param specification from the desc of the function
		String methodReturnType = desc.substring(desc.lastIndexOf(")") + 1);
		
		if (opcode == Opcodes.INVOKESPECIAL || !Controller._bwfilter.allow(owner) || 
				(methodReturnType.length() == 1 && methodReturnType.equals("V")))
			return;
		
		if (Controller._debug)
			System.err.println("RETURN :: " + owner + " : " + name + " : " + desc + " => " + methodReturnType);
		
		// The following four lines the first condition added to the body of instrumented code to ensure that
		// the switch for storing bandwidth is turned on
		super.visitFieldInsn(Opcodes.GETSTATIC, "com/mentorgen/tools/profile/Controller", "_bandwidth", "Z");
		Label controllerBWLabel = new Label();
		super.visitJumpInsn(Opcodes.IFEQ, controllerBWLabel);
		
		//	The following lines ensure that the filtering for bandwidth is applied to the code whenever needed.
		//	This prevents the measurements to happen when there is no necessity to add the values to the method
		//	Dictionary and calculate the amount of data that is communicated between bundles internally.
		super.visitFieldInsn(Opcodes.GETSTATIC, 
				"com/mentorgen/tools/profile/Controller", "_bwfilter", 
				"Lcom/mentorgen/tools/profile/instrument/bwfilter/BandwidthFilter;");
		super.visitInsn(Opcodes.ICONST_2);
		super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
		super.visitInsn(Opcodes.DUP);
		super.visitInsn(Opcodes.ICONST_0);
		super.visitLdcInsn(_className);
		super.visitInsn(Opcodes.AASTORE);
		super.visitInsn(Opcodes.DUP);
		super.visitInsn(Opcodes.ICONST_1);
		super.visitLdcInsn(owner);
		super.visitInsn(Opcodes.AASTORE);
		super.visitMethodInsn(Opcodes.INVOKEINTERFACE, 
				"com/mentorgen/tools/profile/instrument/bwfilter/BandwidthFilter", 
				"accept", "([Ljava/lang/Object;)Z");
		super.visitJumpInsn(Opcodes.IFEQ, controllerBWLabel);
		Label insideCode = new Label();
		super.visitLabel(insideCode);
		
		
		// if return value is a primitive data type
		if (methodReturnType.length() == 1){
			// the following lines just add a constant value to the stack for addMethodOutData to calculate its size
			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);
			this.visitLdcInsn(owner);
			this.visitLdcInsn(name);
			this.visitIntInsn(Opcodes.SIPUSH, PrefSignatureVisitor.getSigSize(methodReturnType.charAt(0)));
			this.visitInsn(Opcodes.I2L); 
			this.visitInsn(Opcodes.ICONST_0);
			super.visitMethodInsn(INVOKESTATIC, 
					"com/mentorgen/tools/profile/runtime/EntityDicHelper", "addMethod2MethodData", 
					"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V");
		}
		// if it is a complex data object
		else{
			
			// The following fifteen lines add filtering for bandwidth data exchange
			// based on the desired level of granularity as specified in the config file for the profiler. 
			// It could be at the level of methods, classes, or class loaders
			
			// The following lines calculates the return value for the method
			int rtID = _lvs.newLocal(Type.getReturnType(desc));
			this.visitVarInsn(Opcodes.ASTORE, rtID);
			this.visitVarInsn(Opcodes.ALOAD, rtID);
			this.visitMethodInsn(Opcodes.INVOKESTATIC,
					"com/mentorgen/tools/profile/instrument/memory/MemoryCounter", "estimate", "(Ljava/lang/Object;)J");
			super.visitInsn(Opcodes.LCONST_0);
			this.visitInsn(Opcodes.LADD);
			this.visitVarInsn(Opcodes.LSTORE, totalSize);
			
			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);
			this.visitLdcInsn(owner);
			this.visitLdcInsn(name);
			this.visitVarInsn(Opcodes.LLOAD, totalSize);
			this.visitInsn(Opcodes.ICONST_0);
			super.visitMethodInsn(INVOKESTATIC, 
					"com/mentorgen/tools/profile/runtime/EntityDicHelper", "addMethod2MethodData", 
					"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V");
			
			// putting the return value back on the stack for the following functions to continue on
			this.visitVarInsn(Opcodes.ALOAD, rtID);
		}
		super.visitLabel(controllerBWLabel);
	}

	//
	// code to handle unwinding the call stack when an exception is thrown
	// (many thanks to Fredrik Svar�n for posting this code in the help forum!)
	//

	@Override
	public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
		super.visitTryCatchBlock(start, end, handler, type);

		// Note: static initializers aren't measured, so make sure that the exception
		// isn't being caught in one
		if (type != null && !_clinit) {
			handler.info = new ExceptionInfo(type);
		}
	}

	@Override
	public void visitLabel(Label label) {
		super.visitLabel(label);

		if (label.info instanceof ExceptionInfo) {
			this.visitLdcInsn(_className);
			this.visitLdcInsn(_methodName);
			this.visitLdcInsn(((ExceptionInfo)label.info).type);

			this.visitMethodInsn(INVOKESTATIC,
					Controller._profiler,
					"unwind",
					"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
			
		}
	}

	class ExceptionInfo {
		String type;
		ExceptionInfo(String type) {
			this.type = type;
		}
	}
	
	private PrefSignatureVisitor parseAndMeasureBandwidth(int opcode, String name, String desc){

		SignatureReader sigReader = new SignatureReader(desc);

		PrefSignatureVisitor methodSigVisitor = new PrefSignatureVisitor();
		sigReader.accept(methodSigVisitor);

		return methodSigVisitor;
	}


	//
	// private methods
	//

	private static boolean isWaitInsn(int opcode, String owner, String name, String desc) {
		boolean isWait = (opcode == Opcodes.INVOKEVIRTUAL
				&& "java/lang/Object".equals(owner)
				&& "wait".equals(name)
				&& ("()V".equals(desc) || "(J)V".equals(desc) || "(JI)V".equals(desc)));
		if (isWait) return true;

		isWait = (opcode == Opcodes.INVOKEVIRTUAL
				&& "java/lang/Thread".equals(owner)
				&& "join".equals(name)
				&& ("()V".equals(desc) || "(J)V".equals(desc) || "(JI)V".equals(desc)));
		if (isWait) return true;

		isWait = (opcode == Opcodes.INVOKESTATIC
				&& "java/lang/Thread".equals(owner)
				&& "sleep".equals(name)
				&& ("(J)V".equals(desc) || "(JI)V".equals(desc)));
		if (isWait) return true;

		isWait = (opcode == Opcodes.INVOKESTATIC
				&& "java/lang/Thread".equals(owner)
				&& "yield".equals(name)
				&& "()V".equals(desc));
		if (isWait) return true;

		return isWait;
	}
}

